/*
 * WebComponent.java
 *
 * Created on March 26, 2007, 6:04 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.atomojo.app;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.atomojo.app.admin.AdminApplication;
import org.atomojo.app.admin.ServerAdminApplication;
import org.atomojo.app.auth.AuthException;
import org.atomojo.app.auth.AuthProtocolService;
import org.atomojo.app.auth.AuthService;
import org.atomojo.app.auth.UserGuard;
import org.atomojo.app.client.Entry;
import org.atomojo.app.client.FeedClient;
import org.atomojo.app.client.FeedDestination;
import org.atomojo.app.client.Link;
import org.atomojo.app.client.Term;
import org.atomojo.app.client.Text;
import org.atomojo.app.db.DB;
import org.atomojo.app.db.DBAuthService;
import org.atomojo.app.db.DBInfo;
import org.atomojo.app.edit.EditApplication;
import org.atomojo.app.sync.FeedSynchronizer;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.restlet.Component;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.Server;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Protocol;
import org.restlet.data.Status;
import org.restlet.routing.Router;
import org.restlet.routing.Template;
import org.restlet.routing.VirtualHost;

/**
 *
 * @author alex
 */
public class WebComponent extends Component {
   
   public static final String LOG_NAME = "org.atomojo.app.www";
   public static final String ATOMOJO_TMP_DIR = "org.atomojo.app.tmp.dir";
   
   static final URI DB_TERM = URI.create("http://www.atomojo.org/O/configuration/db");
   static final URI DB_AUTH_TERM = URI.create("http://www.atomojo.org/O/configuration/db/auth");
   static final URI DB_NAME_TERM = URI.create("http://www.atomojo.org/O/configuration/db/name");
   static final URI DB_GROUP_TERM = URI.create("http://www.atomojo.org/O/configuration/db/group");
   static final URI DB_ALIAS_TERM = URI.create("http://www.atomojo.org/O/configuration/db/alias");
   static final URI HOST_TERM = URI.create("http://www.atomojo.org/O/configuration/host");
   static final URI REALM_TERM = URI.create("http://www.atomojo.org/O/configuration/realm");
   static final URI REALM_NAME_TERM = URI.create("http://www.atomojo.org/O/configuration/realm/name");
   static class AutoConfiguredHost {
      VirtualHost vhost;
      ServerConfiguration.Host host;
      Date edited;
      AutoConfiguredHost(VirtualHost vhost,ServerConfiguration.Host host,Date edited)
      {
         this.vhost = vhost;
         this.host = host;
         this.edited = edited;
      }
   }
   
   class AutoConfiguration {
      Link link;
      Map<String,AutoConfiguredHost> hosts;
      AutoConfiguration(Link link)
      {
         this.link = link;
         this.hosts = new TreeMap<String,AutoConfiguredHost>();
      }
      
      public void configure() {
         FeedClient client = new FeedClient(link.getLink());
         client.setIdentity(link.getUsername(),link.getPassword());
         
         getLogger().info("Loading configuration from "+link.getLink());
         
         try {
            boolean changes = false;
            final List<Entry> hostsToDo = new ArrayList<Entry>();
            final List<Entry> dbToDo = new ArrayList<Entry>();
            final Map<String,Boolean> currentAuthList = new TreeMap<String,Boolean>();
            final Map<String,Boolean> changedAuth = new TreeMap<String,Boolean>();
            client.get(new FeedDestination() {
               public void onFeed(Document feedDoc) {
               }
               public void onEntry(Document entryDoc) {
                  Entry entry = new Entry(entryDoc);
                  entry.index();
                  if (entry.getTerm(HOST_TERM)!=null) {
                     // store host entry till DB entries are done
                     hostsToDo.add(entry);
                     return;
                  }
                  if (entry.getTerm(DB_TERM)!=null) {
                     // store db entry till realms are done
                     dbToDo.add(entry);
                     return;
                  }
                  
                  if (entry.getTerm(REALM_TERM)==null) {
                     // ignore non-realm entry
                     return;
                  }
                  Term nameT = entry.getTerm(REALM_NAME_TERM);
                  if (nameT==null) {
                     return;
                  }
                  String name = nameT.getFirstValue();
                  List<Link> services = entry.getLinks().get("service");
                  if (services==null) {
                     getLogger().warning("Missing 'service' relation link on realm "+name);
                  } else {
                     URI service = services.get(0).getLink();
                     AuthProtocolService auth = (AuthProtocolService)autoServices.get(name);
                     if (auth!=null) {
                        if (!auth.getServiceURI().equals(service)) {
                           // The service URI has changed
                           autoServices.remove(name);
                           auth = null;
                           changedAuth.put(name,Boolean.TRUE);
                        }
                     }
                     if (auth==null) {
                        getLogger().info("Adding realm "+name+" from service "+service);
                        auth = new AuthProtocolService();
                        Properties props = new Properties();
                        props.setProperty("href",service.toString());
                        try {
                           auth.init(props);
                           autoServices.put(name,auth);
                           currentAuthList.put(name,Boolean.TRUE);
                        } catch (AuthException ex) {
                           getLogger().log(Level.SEVERE,"Cannot initialize auth service for realm "+name,ex);
                        }
                     }
                  }
               }
            });
            
            // Configure the databases
            final Map<String,Boolean> currentDBList = new TreeMap<String,Boolean>();
            for (Entry entry : dbToDo) {
               Term nameT = entry.getTerm(DB_NAME_TERM);
               if (nameT==null) {
                  getLogger().warning("Ignoring db entry that does not have name term "+DB_NAME_TERM);
                  return;
               }
               Term authT = entry.getTerm(DB_AUTH_TERM);
               if (authT==null) {
                  getLogger().warning("Ignoring db entry that does not have auth term "+DB_AUTH_TERM);
                  return;
               }
               Term groupT = entry.getTerm(DB_GROUP_TERM);
               Term aliasT = entry.getTerm(DB_ALIAS_TERM);
               String dbName = nameT.getFirstValue();
               String authName = authT.getFirstValue();
               if (dbName==null) {
                  getLogger().warning("Ignoring db entry where name term "+DB_NAME_TERM+" does not have a value.");
                  return;
               }
               if (authName==null) {
                  getLogger().warning("Ignoring db entry where auth term "+DB_NAME_TERM+" does not have a value.");
                  return;
               }
               AuthService auth = services.get(authName);
               if (auth==null) {
                  auth = autoServices.get(authName);
               }
               if (auth==null) {
                  getLogger().warning("Cannot configure db entry for "+dbName+" as auth service "+authName+" does not exist.");
                  return;
               }
               DBInfo dbinfo = autodbList.get(dbName);
               Storage storage = null;
               if (dbinfo==null) {
                  getLogger().info("Creating DB "+dbName);
                  DB db = new DB(getLogger(),new File(dbDir,dbName));
                  dbinfo = new DBInfo(dbName,db,auth,groupT!=null ?  groupT.getFirstValue() : null, aliasT!=null ? aliasT.getFirstValue() : null);
                  storage = storageFactory.getStorage(db);
                  try {
                     getLogger().info("Connecting to DB "+dbName);
                     db.connect();
                     getLogger().info("Connected to DB "+dbName);
                     autodbList.put(dbName,dbinfo);
                     storage.start();
                  } catch (SQLException ex) {
                     getLogger().log(Level.SEVERE,"Cannot connect to database "+dbName,ex);
                  }
               } else {
                  storage = storageFactory.getStorage(dbinfo.getDB());
               }
               currentDBList.put(dbName,Boolean.TRUE);
               UserGuard adminGuard = new UserGuard(childContext,ChallengeScheme.HTTP_BASIC,"Atom Administrator",auth);
               adminGuard.getRequiredGroups().add(AuthService.ADMIN_GROUP);

               adminGuard.setNext(new AdminApplication(childContext,dbinfo.getDB(),storage,AtomApplication.RESOURCE_BASE));
               admins.put(dbName,adminGuard);
               for (Router router : adminRouters) {
                  router.attach("/admin/database/"+dbName,adminGuard).getTemplate().setMatchingMode(Template.MODE_STARTS_WITH);
               }
            }
            
            // Remove databases that have been removed from feed
            for (String name : autodbList.keySet()) {
               if (currentDBList.get(name)==null) {
                  // the database was removed
                  DBInfo dbinfo = autodbList.remove(name);
                  dbinfo.getDB().stop();
                  Storage storage = storageFactory.getStorage(dbinfo.getDB());
                  storage.stop();
                  Restlet admin = admins.remove(name);
                  if (admin!=null) {
                     for (Router router : adminRouters) {
                        router.detach(admin);
                     }
                  }
               }
            }
            
            final Map<String,Boolean> currentEntries = new TreeMap<String,Boolean>();
            for (Entry entry : hostsToDo) {
               Text text = entry.getContent();
               if (text==null || !text.isXML()) {
                  getLogger().warning("Ignoring host entry with missing or has incorrectly typed content for configuration.");
                  continue;
               }
               Element hostE = text.getContent();
               if (hostE==null) {
                  getLogger().warning("Ignoring host entry with empty content.");
                  continue;
               }

               Date edited = null;

               try {
                  edited = entry.getEdited();
                  if (edited==null) {
                     edited = entry.getUpdated();
                  }
               } catch (ParseException ex) {
                  getLogger().warning("Ignoring host entry with malformed date in updated or edited element: "+ex.getMessage());
                  continue;
               }

               if (edited==null) {
                  getLogger().warning("Ignoring host entry without updated or edited element.");
                  continue;
               }

               currentEntries.put(entry.getId(),Boolean.TRUE);

               AutoConfiguredHost autoHost = hosts.get(entry.getId());
               if (autoHost!=null && (autoHost.edited.getTime()<edited.getTime() || changedAuth.get(autoHost.host.getDatabaseName())!=null)) {
                  // the entry has changed, so delete
                  getLogger().info("Entry changed for host "+autoHost.vhost.getHostDomain()+", reconfiguring.");
                  getHosts().remove(autoHost.vhost);
                  autoHost = null;
               }

               if (autoHost!=null) {
                  getLogger().info("Nothing has changed for host "+autoHost.vhost.getHostDomain());
                  continue;
               }

               // Create configuration
               changes = true;

               ServerConfiguration.Host host = config.createHost(hostE);

               // Configure host
               getLogger().info("Adding host "+host.getName());
               VirtualHost vhost = createVirtualHost(host);

               WebComponent.this.configure(vhost,host);

               if (autoHost==null) {
                  hosts.put(entry.getId(),new AutoConfiguredHost(vhost,host,edited));
               } else {
                  autoHost.vhost = vhost;
                  autoHost.host = host;
                  autoHost.edited = edited;
               }

               getHosts().add(vhost);
               
            }
            
            
            // remove all entries that have been deleted
            List<String> toRemove = new ArrayList<String>();
            for (String id : hosts.keySet()) {
               if (currentEntries.get(id)==null) {
                  AutoConfiguredHost autoHost = hosts.get(id);
                  getLogger().info("Removing host "+autoHost.vhost.getHostDomain());
                  changes = true;
                  toRemove.add(id);
                  getHosts().remove(autoHost.vhost);
               }
            }
            for (String id : toRemove) {
               hosts.remove(id);
            }
            
            if (changes && WebComponent.this.isStarted()) {
               getLogger().info("Updating hosts...");
               WebComponent.this.updateHosts();
            }
            
         } catch (Exception ex) {
            getLogger().log(Level.SEVERE,"Fatal error while getting auto configuration feed at "+link.getLink(),ex);
         }
         getLogger().info("Configuration "+link.getLink()+" loaded.");
      }
   }
   
   class AutoConfProcess implements Runnable {
      boolean run = true;
      public void run() {
         while (run) {
            try {
               for (AutoConfiguration autoconf : autoconfs) {
                  autoconf.configure();
                  if (!run) {
                     break;
                  }
               }
               getLogger().info("Waiting "+(config.getAutoConfigurationCheckWait()/1000)+"s to check configuration.");
               Thread.currentThread().sleep(config.getAutoConfigurationCheckWait());
            } catch (InterruptedException ex) {
               
            }
            
         }
         getLogger().info("Auto configuration thread exiting.");
      }
   }
   
   List<AutoConfiguration> autoconfs;
   AutoConfProcess autoconfProc;
   Thread autoconfThread;

   Map<String,DB> dbList;
   Map<String,DBInfo> dbConfList;
   Map<String,DBInfo> autodbList;
   Map<String,Storage> storageList;
   Map<String,Storage> autoStorageList;
   StorageFactory storageFactory;
   FeedSynchronizer sync;
   Thread syncThread;
   ServerConfiguration config;
   //Router adminRouter;
   Map<String,AuthService> services;
   Map<String,AuthService> autoServices;
   Map<String,Restlet> admins;
   File dbDir;
   List<Router> adminRouters;
   AtomicBoolean memoryManagerRunning;
   Thread memoryManager;
   
   Map<String,FileWriter> logs;
   Context childContext;

   Map<String,ComponentModule> modules;
   
   /** Creates a new instance of WebComponent */
   public WebComponent(File dbDir,Map<String,DB> dbList,StorageFactory storageFactory,ServerConfiguration serverConf) {
      this.dbDir = dbDir;
      this.dbList = dbList;
      this.dbConfList = new TreeMap<String,DBInfo>();
      this.autodbList = new TreeMap<String,DBInfo>();
      this.storageList = new TreeMap<String,Storage>();
      this.storageList = new TreeMap<String,Storage>();
      this.storageFactory = storageFactory;
      this.config = serverConf;
      this.autoconfs = new ArrayList<AutoConfiguration>();
      this.adminRouters = new ArrayList<Router>();
      getLogService().setLoggerName(LOG_NAME);
      getLogService().setEnabled(false);
      this.logs = new TreeMap<String,FileWriter>();
      this.childContext = getContext().createChildContext();
      this.modules = new HashMap<String,ComponentModule>();

      try {
         this.storageFactory.init(childContext);
      } catch (Exception ex) {
         getLogger().log(Level.SEVERE,"Cannot initialize storage factory with context.",ex);
      }
      
      // TODO: the keystore should be part of the server.conf
      getContext().getParameters().add("keystorePath",serverConf.getKeystoreFile().getAbsolutePath());
      getContext().getParameters().add("keystorePassword",serverConf.getKeystorePassword());
      getContext().getParameters().add("keyPassword",serverConf.getKeyPassword());
      for (ServerConfiguration.Interface iface : serverConf.getInterfaces()) {
         if (iface.isSecure()) {
            getLogger().info("https listening on "+iface.getAddress()+":"+iface.getPort());
            Server server = getServers().add(Protocol.HTTPS, iface.getAddress().equals("*") ? null : iface.getAddress(), iface.getPort());            
            server.getContext().getParameters().add("keystorePath",serverConf.getKeystoreFile().getAbsolutePath());
            server.getContext().getParameters().add("keystorePassword",serverConf.getKeystorePassword());
            server.getContext().getParameters().add("keyPassword",serverConf.getKeyPassword());
         } else {
            getLogger().info("http listening on "+iface.getAddress()+":"+iface.getPort());
            getServers().add(Protocol.HTTP, iface.getAddress().equals("*") ? null : iface.getAddress(), iface.getPort());            
         }
      }
      for (Server server : getServers()) {
         try {
            server.start();
         } catch (Exception ex) {
            getLogger().log(Level.SEVERE,"Cannot start server.",ex);
         }
      }
      
      // Add the clients
      getClients().add(Protocol.FILE);
      for (Protocol protocol : storageFactory.getStorageProtocols()) {
         getClients().add(protocol);
      }

      childContext.getAttributes().put(ATOMOJO_TMP_DIR,serverConf.getTempDirectory());

      services = new HashMap<String,AuthService>();
      autoServices = new HashMap<String,AuthService>();
      
      for (ServerConfiguration.Auth auth : serverConf.getAuthServices().values()) {
         try {
            AuthService service = auth.newInstance();
            services.put(auth.getName(),service);
         } catch (AuthException ex) {
            getLogger().log(Level.SEVERE,"Cannot instantiate auth service "+auth.getName(),ex);
         }
      }
      ServerAdminApplication admin = new ServerAdminApplication(childContext,dbConfList,autodbList,storageFactory);
      for (ServerConfiguration.AdminHost adminHost : serverConf.getAdminHosts().values()) {
         
         VirtualHost vhost =  createVirtualHost(adminHost);
         getHosts().add(vhost);
         adminRouters.add(vhost);
         String authName = adminHost.getAuthName();
         if (authName==null) {
            getLogger().severe("The admin interface is missing a named auth service.");
            continue;
         }
         AuthService service = services.get(authName);
         if (service==null) {
            getLogger().severe("Cannot find auth service "+authName+" for admin interface.");
            continue;
         }
         UserGuard adminGuard = new UserGuard(childContext,ChallengeScheme.HTTP_BASIC,"Atom Administrator",service);
         adminGuard.getRequiredGroups().add(AuthService.ADMIN_GROUP);
         adminGuard.setNext(admin);
         
         vhost.attach("/admin",adminGuard);
         
      }
      
      admins = new HashMap<String,Restlet>();
      Restlet lastAdmin = null;
      for (final DB adminDB : dbList.values()) {
         getLogger().info("Configuring database "+adminDB.getName()+" for administration");
         ServerConfiguration.Database databaseConf = serverConf.getDatabases().get(adminDB.getName());
         AuthService service = null;
         if (databaseConf==null || databaseConf.getAuthName()==null) {
            service = new DBAuthService();
            Properties props = new Properties();
            props.setProperty("database",adminDB.getName());
            props.setProperty("dir", adminDB.getDatabaseDir().getAbsolutePath());
            try {
               service.init(props);
            } catch (AuthException ex) {
               getLogger().log(Level.SEVERE,"Cannot instantiate auth service for database "+adminDB.getName(),ex);
               continue;
            }
         } else {
            service = services.get(databaseConf.getAuthName());
            if (service==null) {
               getLogger().severe("Cannot find auth service "+databaseConf.getAuthName()+" for database "+adminDB.getName());
               continue;
            }
         }
         DBInfo dbinfo = new DBInfo(adminDB.getName(),adminDB,service,databaseConf==null ? null : databaseConf.getGroup(),databaseConf==null ? null : databaseConf.getGroupAlias());
         dbConfList.put(dbinfo.getName(),dbinfo);
         UserGuard adminGuard = new UserGuard(childContext,ChallengeScheme.HTTP_BASIC,"Atom Administrator",service);
         adminGuard.getRequiredGroups().add(AuthService.ADMIN_GROUP);

         try {
            adminGuard.setNext(new AdminApplication(childContext,adminDB,storageFactory.getStorage(adminDB),AtomApplication.RESOURCE_BASE));
            admins.put(adminDB.getName(),adminGuard);
            for (Router router : adminRouters) {
               router.attach("/admin/database/"+adminDB.getName(),adminGuard).getTemplate().setMatchingMode(Template.MODE_STARTS_WITH);
            }
            lastAdmin = adminGuard;
         } catch (Exception ex) {
            getLogger().log(Level.SEVERE,"Cannot add admin due to exception.",ex);
         }
      }
      
      // Add the configured hosts
      for (ServerConfiguration.Host hostConf : serverConf.getHosts().values()) {
         
         VirtualHost vhost =  createVirtualHost(hostConf);
         getHosts().add(vhost);
         try {
            configure(vhost,hostConf);
         } catch (Exception ex) {
            getLogger().log(Level.SEVERE,"Cannot configure host "+hostConf.getName()+" due to exception.",ex);
         }
      }
      
      // Add the configured hosts
      for (ServerConfiguration.ResourceHost resourceConf : serverConf.getResourceHosts().values()) {

         VirtualHost vhost =  createVirtualHost(resourceConf);
         getHosts().add(vhost);
         try {
            configureResource(vhost,resourceConf);
         } catch (Exception ex) {
            getLogger().log(Level.SEVERE,"Cannot configure host "+resourceConf.getName()+" due to exception.",ex);
         }
      }

      for (Link link : serverConf.getAutoConfiguration()) {
         addAutoConfiguration(link);
      }
      
   }
   
   protected VirtualHost createVirtualHost(ServerConfiguration.Host host)
   {
      VirtualHost vhost = new VirtualHost(childContext);
      getLogger().info("Creating virtual route for "+host.getName()+":"+host.getPort());
      if (host.getAddress()!=null) {
         getLogger().info("Restricting to address "+host.getAddress());
         vhost.setServerAddress(host.getAddress());
      }
      if (host.getPort()>0) {
         getLogger().info("Restricting to port "+host.getPort());
         vhost.setHostPort(Integer.toString(host.getPort()));
      } else {
         vhost.setHostPort(".*");
      }
      if (host.getName()!=null && !host.getName().equals("*")) {
         vhost.setHostDomain(host.getName());
      }
      return vhost;
      
   }
   
   protected void configure(VirtualHost vhost,ServerConfiguration.Host host)
      throws Exception
   {
      DBInfo atomDB = dbConfList.get(host.getDatabaseName());
      if (atomDB==null) {
         atomDB = autodbList.get(host.getDatabaseName());
      }
      if (atomDB==null) {
         throw new RuntimeException("Database "+host.getDatabaseName()+" does not exist.");
      }
      AuthService auth = atomDB.getAuthService();
      if (auth==null) {
         throw new RuntimeException("The auth service for database "+host.getDatabaseName()+" does not exist.");
      }
      
      Storage storage = storageFactory.getStorage(atomDB.getDB());


      Restlet adminApp = admins.get(atomDB.getName());
      if (adminApp==null) {
         getLogger().warning("There is no admin restlet for "+atomDB.getName());
      }
      
      AtomApplication app = new AtomApplication(childContext,auth,atomDB.getDB(),storage,host.allowQueries());

      Router router = new Router(childContext);
      router.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
      router.attachDefault(app);
      router.attach("/admin",adminApp).getTemplate().setMatchingMode(Template.MODE_STARTS_WITH);

      if (host.allowEditClient()) {
         router.attach("/edit",new EditApplication(childContext,app));
      }

      Logger hostLog = Logger.getLogger("atomojo.host."+host.getName());
      HostLogFilter logFilter = new HostLogFilter(hostLog);
      logFilter.setNext(router);
      
      vhost.attach(logFilter);
      
      addHostLog(host.getName(),hostLog,host.getLogFile());
      
   }

   protected Restlet makeApp(DBInfo info,boolean allowQueries,boolean allowEditClient,String logName)
      throws Exception
   {
      Storage storage = storageFactory.getStorage(info.getDB());
      AtomApplication app = new AtomApplication(childContext,info.getAuthService(),info.getDB(),storage,allowQueries);

      Restlet adminApp = admins.get(info.getName());
      if (adminApp==null) {
         getLogger().warning("There is no admin restlet for "+info.getName());
      }

      Router router = new Router(childContext);
      router.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
      router.attachDefault(app);
      router.attach("/admin",adminApp);

      if (allowEditClient) {
         router.attach("/edit",new EditApplication(childContext,app));
      }

      Logger hostLog = Logger.getLogger("atomojo.host."+logName);
      HostLogFilter logFilter = new HostLogFilter(hostLog);
      logFilter.setNext(router);
      
      return logFilter;

   }

   protected void configureResource(VirtualHost vhost,final ServerConfiguration.ResourceHost host)
      throws Exception
   {
      getLogger().info("Creating virtual route for resource host "+host.getName()+":"+host.getPort());

      // TODO: this should be more dynamic with autoconf
      final Map<String,Restlet> apps = new TreeMap<String,Restlet>();

      final boolean checkNames = host.getDatabases().size()>0 ? true : false;
      for (DBInfo info : dbConfList.values()) {
         if (checkNames && !host.getDatabases().contains(info.getName())) {
            continue;
         }
         String group = info.getGroup();
         String alias = info.getAlias();
         if (group==null) {
            continue;
         }
         if (alias==null) {
            alias = info.getName();
         }
         String key = group+"."+alias;
         getLogger().info("Adding static APP key "+key);
         Restlet app = makeApp(info,host.allowQueries(),host.allowEditClient(),key);
         apps.put(key,app);
      }
      vhost.attach("/{group}/{alias}",new Restlet() {
         public void handle(Request request, Response response) {
            try {
               String rgroup = request.getAttributes().get("group").toString();
               String ralias = request.getAttributes().get("alias").toString();
               String rkey = rgroup+"."+ralias;
               Restlet restlet = apps.get(rkey);
               if (restlet==null) {
                  for (DBInfo info : autodbList.values()) {
                     if (checkNames && !host.getDatabases().contains(info.getName())) {
                        continue;
                     }
                     String group = info.getGroup();
                     String alias = info.getAlias();
                     if (group==null) {
                        continue;
                     }
                     if (alias==null) {
                        alias = info.getName();
                     }
                     String key = group+"."+alias;
                     Restlet app = makeApp(info,host.allowQueries(),host.allowEditClient(),key);
                     getLogger().info("Adding dynamic APP key "+key);
                     apps.put(key,app);
                     restlet = app;
                  }
               }
               if (restlet!=null) {
                  restlet.handle(request, response);
               } else {
                  response.setStatus(Status.CLIENT_ERROR_NOT_FOUND,"Cannot find APP instance for group "+rgroup+" and alias "+ralias);
               }
            } catch (Exception ex) {
               getLogger().log(Level.SEVERE,"Exception while checking auto configuration list.",ex);
            }
         }
      });

   }


   public void addHostLog(final String name,Logger log,File file)
      throws IOException
   {
      if (!file.getParentFile().exists()) {
         file.getParentFile().mkdirs();
      }
      final FileWriter w = new FileWriter(file,file.exists() ? true : false);
      logs.put(name,w);
      log.addHandler(new Handler() {
         public void publish(LogRecord record) {
            if (record.getLevel().intValue()==Level.FINE.intValue()) {
               try {
                  w.write(record.getMessage());
                  w.write('\n');
                  w.flush();
               } catch (IOException ex) {
                  ex.printStackTrace();
               }
            }
         }

         public void flush() {
            try {
               w.flush();
            } catch (IOException ex) {
               ex.printStackTrace();
            }
         }

         public void close() {
            if (logs.get(name)!=null) {
               try {
                  w.close();
                  logs.remove(name);
               } catch (IOException ex) {
                  ex.printStackTrace();
               }
            }
         }
      });
   }
   
   void addAutoConfiguration(Link link)
   {
      AutoConfiguration autoconf = new AutoConfiguration(link);
      autoconfs.add(autoconf);
   }

   protected void registerModule(Class<? extends ComponentModule> moduleClass)
      throws InstantiationException,IllegalAccessException
   {
      ComponentModule module = moduleClass.newInstance();
      if (modules.get(module.getName())!=null) {
         getLogger().info("Ignoring module, already mapped: "+module.getName()+" -> "+module.getClass().getName());
         return;
      }
      getLogger().info("Registering module: "+module.getName()+" -> "+module.getClass().getName());
      modules.put(module.getName(), module);
   }
   protected void loadModules() 
      throws IOException
   {
      Enumeration<URL> locations = getClass().getClassLoader().getResources("META-INF/services/"+ComponentModule.class.getName());
      while (locations.hasMoreElements()) {
         URL location = locations.nextElement();
         BufferedReader reader = null;
         try {
             reader = new BufferedReader(new InputStreamReader(location.openStream(), "UTF-8"));
             String className = reader.readLine();

             while (className != null) {
                 int hashPosition = className.indexOf('#');
                 if (hashPosition>0) {
                     className = className.substring(0, hashPosition);
                 }
                 className = className.trim();
                 if (className.length()>0) {
                    getLogger().fine("Loading module class: "+className);
                    try {
                       Class moduleClass = getClass().getClassLoader().loadClass(className);
                       registerModule(moduleClass);
                    } catch (ClassNotFoundException ex) {
                       getLogger().warning("Cannot load module due to missing class: "+ex.getMessage());
                    } catch (Exception ex) {
                       getLogger().log(Level.SEVERE,"Cannot load module due to exception.",ex);
                    }
                 }
                 className = reader.readLine();
             }
         } finally {
             if (reader != null) {
                 reader.close();
             }
         }
      }

   }
   
   public void start()
      throws Exception
   {
      loadModules();
      
      for (ComponentModule module : modules.values()) {
         module.init(getContext(),getInternalRouter());
         module.start();
      }
      for (DBInfo dbinfo : dbConfList.values()) {
         Storage storage = storageFactory.getStorage(dbinfo.getDB());
         storage.start();
      }
      super.start();
      if (autoconfs.size()>0) {
         getLogger().info("Starting auto-configuration process...");
         autoconfProc = new AutoConfProcess();
         autoconfThread = new Thread(autoconfProc);
         autoconfThread.start();
         getLogger().info("...started.");
      }
      memoryManagerRunning = new AtomicBoolean(true);
      memoryManager = new Thread(new Runnable() {
         long sleepWait = 3*60*1000;
         public void run() {
            while (memoryManagerRunning.get()) {
               try {
                  Thread.currentThread().sleep(sleepWait);
                  System.gc();
               } catch (InterruptedException ex) {
               }
            }
         }
      });
      memoryManager.start();
   }
   public void stop() 
      throws Exception
   {
      for (String name : logs.keySet()) {
         FileWriter out = logs.get(name);
         try {
            out.close();
         } catch (IOException ex) {
            getLogger().log(Level.SEVERE,"Cannot close log file for host "+name,ex);
         }
      }
      if (autoconfs.size()>0) {
         getLogger().info("Stopping auto-configuration process...");
         autoconfProc.run = false;
         synchronized (autoconfThread) {
            autoconfThread.interrupt();
         }
         autoconfThread.join(2000);
         getLogger().info("...stopped.");
      }
      memoryManagerRunning.set(false);
      memoryManager.interrupt();
      for (DBInfo dbinfo : dbConfList.values()) {
         dbinfo.getDB().stop();
         Storage storage = storageFactory.getStorage(dbinfo.getDB());
         storage.stop();
      }
      for (DBInfo dbinfo : autodbList.values()) {
         dbinfo.getDB().stop();
         Storage storage = storageFactory.getStorage(dbinfo.getDB());
         storage.stop();
      }
      for (ComponentModule module : modules.values()) {
         module.init(getContext(),getInternalRouter());
         module.stop();
      }
      memoryManager.join(2000);
      super.stop();
   }
   
}
